package system.track;

import java.util.LinkedList;
import system.train.Train;
import system.communication.Monitor;
import system.ui.Engine;

/**
 * A block represents a track block in memory.
 * @author David Kolb
 */
public class Block
{
    private int id;
    private Switch ccwSwitch;
    private Switch cwSwitch;
    private Direction state;
    private int speed;
    private Train theTrain;
    private LinkedList<Sensor> ccwSensors;
    private LinkedList<Sensor> cwSensors;
    private volatile boolean reserved;
    
    public Block()
    {
        ccwSensors = new LinkedList<Sensor>();
        cwSensors = new LinkedList<Sensor>();
        id = 0;
        state = Direction.CLOCKWISE;
        speed = 0;
        reserved = false;
    }

    public Block(int id)
    {
        this();
        setID(id);
    }

    /**
     * Registers an integer id with the block.
     * 
     * @param id an ID that corresponds to the blocks ID in the layout. 
     */
    public void setID(int id)
    {
        this.id = id;
        setDirection(state, speed);
    }
    
    /**
     * Retrieves the blocks id.
     * 
     * @return an integer that corresponds to a block in the layout.
     */
    public int getID()
    {
        return id;
    }

    /**
     * Registers a switch as being located at the end of the 
     * block in the direction denoted by the argument d..
     *
     * @param d the direction denoting the end that the switch is on.
     * @param trackSwitch the switch located at the end of the block.
     */
    public void setSwitch(Direction d, Switch trackSwitch)
    {
        switch (d)
        {
            case CLOCKWISE:
                this.cwSwitch = trackSwitch;
                break;
            case COUNTERCLOCKWISE:
                this.ccwSwitch = trackSwitch;
                break;
            default:
                throw new IllegalArgumentException("Bad direction arg");
        }
    }

    /**
     * Returns the switch in the specified direction.
     * 
     * @param d the direction to get the switch from.
     * @return The switch to return, null if none.
     */
    public Switch getSwitch(Direction d)
    {
        Switch returnVal = null;
        switch (d)
        {
            case CLOCKWISE:
                returnVal = this.cwSwitch;
                break;
            case COUNTERCLOCKWISE:
                returnVal = this.ccwSwitch;
                break;
            default:
                throw new IllegalArgumentException("Bad direction arg.");
        }
        return returnVal;
    }

    /**
     * Equates to Block#canSetSwitch(Block#getDirection()).
     * @see Block#canSetSwitch(system.track.Direction)
     */

    public boolean canSetSwitch()
    {
        return canSetSwitch(getDirection());
    }

    /**
     * This function returns a boolean informing the caller if the switch
     * in the specified direction is able to be set.
     * 
     * Return values are as follows:
     * <table border="1">
     * <tr>
     *  <td>Conditions</td>
     *  <td>Return Value</td>
     * </tr>
     * <tr>
     *  <td>Switch entrance is orientated twoards block</td>
     *  <td><code>true</code></td>
     * </tr>
     * <tr>
     *  <td>Switch exit is orientated towards block</td>
     *  <td><code>false</code></td>
     * </tr>
     * </table>
     * @return true if there is a switch that can be set
     */
    public boolean canSetSwitch(Direction d)
    {
        boolean returnVal = false;
        try
        {
            switch(d)
            {
                case CLOCKWISE:
                    if (this.cwSwitch.getEntrance() == this)
                    {
                        returnVal = true;
                    }
                    break;
                case COUNTERCLOCKWISE:
                    if (this.ccwSwitch.getEntrance() == this)
                    {
                        returnVal = true;
                    }
            }
        }
        catch (NullPointerException e)
        {
        }
        return returnVal;
    }

    /**
     * This sets the direction to one of the three directions defined
     * in the enum.
     * 
     * @see Direction
     * @param d one of the instantiated objects of the <code>Direction</code> enum.
     * @param speed the speed setting between 0 and 7
     * @throws IllegalArgumentException when speed < 0 || speed > 7
     */
    public synchronized void setDirection(Direction d, int speed)
    {
        if (speed > 7 || speed < 0)
        {
            throw new IllegalArgumentException("Bad speed argument: " + speed);
        }
        this.state = d;
        this.speed = speed;
        Monitor.getInstance().setBlock(this.id, d, speed);
        Engine.getInstance().postBlockDirection(id, d);
        Engine.getInstance().postBlockSpeed(id, speed);
    }

    /**
     * This returns the current direction of a track.
     * 
     * @return one of the instantiated object of the <code>Direction</code> enum.
     */ 
    public Direction getDirection()
    {
        return this.state;
    }

    /**
     * This returns based on whether this block is the special case
     * block that can park trains.
     * @return <code>true</code> if the block can park trains
     */
    public boolean isParkingBlock()
    {
        return false;
    }
    
    /**
     * In the case that the block can park a train, this instructs the
     * block as to whether or not it's parking a train.
     * 
     * @param flag <code>true</code> to park the train, <code>false</code> to 
     * allow the train to cross over.
     * @throws track.IllegalFunctionCallException when 
     * <code>isParkingBlock()</code> returns <code>false</code>
     * @see Block#isParkingBlock() 
     */
    public synchronized void setPark(boolean flag) throws IllegalFunctionCallException
    {
        throw new IllegalFunctionCallException("Not a parking block.");
    }

    /**
     * Places a train object on the block.
     * 
     * When a train travels from one block to the next, this is how the train
     * informs the block that it is now running a train.
     * 
     * @param train the train object that is currently travelling on the block.
     * @deprecated reservation system allows for greater flexibilty and
     * does more with less data.  use reserve/inreserve instead.
     */
    @Deprecated
    public void giveTrain(Train train)
    {
        this.theTrain = train;
    }

    /**
     * Returns whether there is a train on the block currently.
     * 
     * @return <code>true</code> if the train is on the block, <code>false</code>
     * if there is no train currently on the block.
     * @deprecated reservation system allows for greater flexibilty and
     * does more with less data.  use reserve/inreserve instead.
     */
    @Deprecated
    public boolean hasTrain()
    {
        return (this.theTrain != null);
    }

    /**
     * Removes a train from a track block.
     * 
     * This will remove the train from a block, returning the train to the
     * caller.
     * 
     * @return the train that was on the block.
     * @deprecated reservation system allows for greater flexibilty and
     * does more with less data.  use reserve/inreserve instead.
     */
    @Deprecated
    public Train takeTrain()
    {
        Train returnVal = this.theTrain;
        this.theTrain = null;
        return returnVal;
    }
    
    /**
     * Returns the set of sensors that can be tripped in
     * the specified direction.
     * @param d  the direction to look for sensors
     * @return a linked list of sensors
     */
    public LinkedList<Sensor> getSensors(Direction d)
    {
        LinkedList<Sensor> returnVal;
        switch(d)
        {
            case CLOCKWISE:
                returnVal = this.cwSensors;
                break;
            case COUNTERCLOCKWISE:
                returnVal = this.ccwSensors;
                break;
            default:
                throw new IllegalArgumentException("Bad direction arg.");
        }
        return returnVal;
    }

    /**
     * Adds a sensor to the set returned by getSensor().
     * @param d the direction to add the sensor to
     * @param s the sensor to add
     */
    public void addSensor(Direction d, Sensor s)
    {
        switch(d)
        {
            case CLOCKWISE:
                this.cwSensors.add(s);
                break;
            case COUNTERCLOCKWISE:
                this.ccwSensors.add(s);
                break;
            default:
                throw new IllegalArgumentException("Bad direction arg.");
        }
    }

    /**
     * This gets the next block in the direction specified.
     *
     * The algorithm for getting the block takes into account the current
     * setting of the switch.  For instance, if the switch at the end is
     * orientated so there are two possible exits and is set to 180 degrees,
     * then the exit at 180 degrees is returned.
     * @param d the direction in which to get the next block.
     * @return the next block
     */
    public Block getNextBlock(Direction d)
    {
        Block returnVal;
        Switch theSwitch;
        switch (d)
        {
            case CLOCKWISE:
                theSwitch = this.cwSwitch;
                break;
            case COUNTERCLOCKWISE:
                theSwitch = this.ccwSwitch;
                break;
            default:
                throw new IllegalArgumentException("Bad direction arg.");

        }
        //Determine orientation.
        if (theSwitch.getEntrance() == this)
        { // We are at the entrance to a switch.
          // Grab the exit the switch is set to.
            returnVal = theSwitch.getExit(theSwitch.getAngle());
        }
        else
        { // We are at an exit to the switch, return the entrance.
            returnVal = theSwitch.getEntrance();
        }
        return returnVal;
    }

    /**
     * Grabs a specific block adjacent to the track.
     *
     * For instance, to grab the block adjacent to this block
     * in the counter clockwise direction while the switch were set to
     * 180 degrees, regardless of the block's or switch's current state,
     * you would call this function as <code>
     * getSpecificBlock(Direction.COUNTERCLOCKWISE, SwitchAngle.ONE_EIGHTY);</code>
     * 
     * If Block#canSetSwitch(d) == false, the parameter a is ignored.
     *
     * @param d the direction to search for a block
     * @param a the angle to search for a block
     * @return the block specified
     */
    public Block getSpecificBlock(Direction d, SwitchAngle a)
    {
        Block returnVal = null;
        if(canSetSwitch(d))
        {
            returnVal = getSwitch(d).getExit(a);
        } else {
            returnVal = getSwitch(d).getEntrance();
        }
        return returnVal;
    }

    /**
     * Attempts to reserve the switch and places the block
     * under the control of a specific thread.
     * @return <code>true</code> if the block is successfully reserved<br/>
     * <code>false</code> if the block is already reserved
     */
    public synchronized boolean reserve()
    {
        boolean returnVal = false;
        if(!reserved)
        {
            returnVal = true;
            reserved = true;
        }
        Engine.getInstance().postBlockReservation(id, reserved);
        return returnVal;
    }

    /**
     *  Releses the control of the switch.
     *
     *  If a the block is reserved then this sets the switch to unreserved.
     */
    public synchronized void unreserve()
    {
        reserved = false;
        Engine.getInstance().postBlockReservation(id, reserved);
    }

    /**
     * This function will set a switch in the direction
     * specified whose exit is orientated towards the
     * block to the setting needed to preven derailing.
     * @param d the direction to search for a switch.
     */
    public synchronized void autoSetSwitch(Direction d)
    {
        if(!canSetSwitch(d))
        {
            Switch workingSwitch = getSwitch(d);
            if(workingSwitch.getExit(SwitchAngle.ONE_EIGHTY_DEGREES) == this)
            {
                workingSwitch.setAngle(SwitchAngle.ONE_EIGHTY_DEGREES);
            }
            else
            {
                workingSwitch.setAngle(SwitchAngle.NINETY_DEGREES);
            }
        }
    }
}
